package model;

import java.awt.Point;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import utils.PlayerSides;
import utils.RandomGenerator;

import model.pieces.King;
import model.pieces.Piece;
import model.pieces.PieceFactory;

/**
 * Class to manage the chess game board. The class hides the implementation of how the pieces are actually saved and moved on the board.
 * The class enables adding, removing and moving pieces
 */
public class BoardManager implements Model, Board {

	private static final short BOARD_SIZE = 8; // size of the square chess board
	private static final int ROOK_COLUMN_RIGHT = BOARD_SIZE;
	private static final int ROOK_COLUMN_LEFT = 1;
	private static final int KNIGHT_COLUMN_RIGHT = BOARD_SIZE-1;
	private static final int KNIGHT_COLUMN_LEFT = 2;
	private static final int BISHOP_COLUMN_RIGHT = BOARD_SIZE-2;
	private static final int BISHOP_COLUMN_LEFT = 3;
	private static final int QUEEN_COLUMN = 4;
	private static final int KING_COLUMN = 5;
	private static final int WHITE_FIRST_ROW = BOARD_SIZE;
	private static final int WHITE_SECOND_ROW = BOARD_SIZE-1;
	private static final int BLACK_FIRST_ROW = 1;
	private static final int BLACK_SECOND_ROW = 2;
	private static final int CHANCE_FOR_CREATURE = 2;

	private Piece[][] chessBoard; // table to hold pieces
	private PieceFactory pieceFactory; // factory to create the pieces 
	private Map<Point, Piece> firstPlayerPieces; // holds first player's pieces by positions 
	private Map<Point, Piece> secondPlayerPieces; // holds second player's pieces by positions
	
	private List<Point> creaturePositions; // holds positions of Creatures
	private List<Point> collidedCreatures; // holds positions of Creatures that collided at current Creature-placing
	
	/**
	 * Gets position and a piece.
	 * If position is valid, piece is placed there and True is returned.
	 * Otherwise, does not place piece and False is returned
	 * @param row- row index for piece
	 * @param col- column index for piece
	 * @param pieceToPlace- reference for the piece to place
	 * @return True if could place piece, False if not
	 */
	private boolean placePiece(int row, int col, Piece pieceToPlace) {
		
		// check if given position is valid
		if (notValidPosition(row, col)) {
			// cannot place given piece
			return false;
		}
				
		// place new piece at given position
		chessBoard[BOARD_SIZE-row][col-1] = pieceToPlace;
		return true;
	}
	
	/**
	 * Sets piece at prevPosition to be placed at newPosition.
	 * If indicated player does not have a piece at given prevPosition, no position is changed
	 * and False is returned. Otherwise, updates piece and returns True
	 * @param side- indicates which player should update  
	 * @param prevPosition- piece's previous position
	 * @param newPosition- piece's new position
	 * @return True if could update piece, False if not
	 */
	private boolean updatePlayerMap(PlayerSides.Sides side, Point prevPosition, Point newPosition) {
		
		// check which player should update
		if (side.equals(PlayerSides.Sides.White)) {
			
			// check if map holds a piece at given position
			if (!firstPlayerPieces.containsKey(prevPosition)) {
				// no piece at given position, cannot update
				return false;
			}
			
			// update piece
			Piece pieceToUpdt = firstPlayerPieces.get(prevPosition); // get piece to update
			firstPlayerPieces.remove(prevPosition); // remove from map
			firstPlayerPieces.put(newPosition, pieceToUpdt); // insert piece with new position
			return true;
		}
		
		// should update second player
		
		// check if map holds a piece at given position
		if (!secondPlayerPieces.containsKey(prevPosition)) {
			// no piece at given position, cannot update
			return false;
		}
		
		// update piece
		Piece pieceToUpdt = secondPlayerPieces.get(prevPosition); // get piece to update
		secondPlayerPieces.remove(prevPosition); // remove from map
		secondPlayerPieces.put(newPosition, pieceToUpdt); // insert piece with new position
		return true;
	}
	
	/**
	 * Constructor- creates a new board manager
	 */
	public BoardManager() {
		
		// allocate a new table to hold pieces
		chessBoard = new Piece[BOARD_SIZE][BOARD_SIZE];
		
		// allocate a piece factory object
		pieceFactory = new PieceFactory();
		
		// allocate maps to hold references to first player's pieces
		firstPlayerPieces = new HashMap<Point, Piece>();
		
		// allocate maps to hold references to second player's pieces
		secondPlayerPieces = new HashMap<Point, Piece>();
		
		// allocate list for Creature positions
		creaturePositions = new LinkedList<Point>();
		
		// allocate list for collided Creature positions
		collidedCreatures = new LinkedList<Point>();
	}
	
	/**
	 * Returns board size
	 */
	@Override
	public int getBoardSize() { return BOARD_SIZE; }
	
	/**
	 * Clears the board from pieces- allocates a new chess board and clears piece maps
	 * for both players
	 */
	@Override
	public void clearBoard() {
		
		// initialize chess board
		chessBoard = new Piece[BOARD_SIZE][BOARD_SIZE];
		
		// clear players' piece maps
		firstPlayerPieces.clear();
		secondPlayerPieces.clear();
	}
	
	/**
	 * Returns the board manager as Board interface
	 */
	@Override
	public Board getBoard() { return this; }
	
	/**
	 * Places all 16 pieces on board (8 for each opponents).
	 * First player's pieces are placed at the upper part of the board (rows 7-8)
	 */
	@Override
	public void setBoardStandard() {

		// clear board
		clearBoard();
		
		// set second and one-before-last rows- place only Pawns
		for (int currCol = 1; currCol <= BOARD_SIZE; ++currCol) {
			
			// place Pawns
			placePiece(WHITE_SECOND_ROW, currCol, pieceFactory.createPiece("Pawn", PlayerSides.Sides.White)); // first player's Pawns
			placePiece(BLACK_SECOND_ROW, currCol, pieceFactory.createPiece("Pawn", PlayerSides.Sides.Black)); // second player's Pawns
			// add Pawns to maps
			firstPlayerPieces.put(new Point(WHITE_SECOND_ROW,currCol), getPiece(WHITE_SECOND_ROW, currCol)); // add to first player's map
			secondPlayerPieces.put(new Point(BLACK_SECOND_ROW,currCol), getPiece(BLACK_SECOND_ROW, currCol)); // add to second player's map
			
		}
		
		// place Rooks
		placePiece(WHITE_FIRST_ROW, ROOK_COLUMN_LEFT, pieceFactory.createPiece("Rook", PlayerSides.Sides.White)); // first player's Rooks 
		placePiece(WHITE_FIRST_ROW, ROOK_COLUMN_RIGHT, pieceFactory.createPiece("Rook", PlayerSides.Sides.White));
		placePiece(BLACK_FIRST_ROW, ROOK_COLUMN_LEFT, pieceFactory.createPiece("Rook", PlayerSides.Sides.Black)); // second player's Rooks
		placePiece(BLACK_FIRST_ROW, ROOK_COLUMN_RIGHT, pieceFactory.createPiece("Rook", PlayerSides.Sides.Black));
		// add Rooks to maps
		firstPlayerPieces.put(new Point(WHITE_FIRST_ROW,ROOK_COLUMN_LEFT), getPiece(WHITE_FIRST_ROW,ROOK_COLUMN_LEFT)); // add to first player's map
		firstPlayerPieces.put(new Point(WHITE_FIRST_ROW,ROOK_COLUMN_RIGHT), getPiece(WHITE_FIRST_ROW,ROOK_COLUMN_RIGHT));
		secondPlayerPieces.put(new Point(BLACK_FIRST_ROW,ROOK_COLUMN_LEFT), getPiece(BLACK_FIRST_ROW, ROOK_COLUMN_LEFT)); // add to second player's map
		secondPlayerPieces.put(new Point(BLACK_FIRST_ROW,ROOK_COLUMN_RIGHT), getPiece(BLACK_FIRST_ROW,ROOK_COLUMN_RIGHT));
		
		// place Knights
		placePiece(WHITE_FIRST_ROW, KNIGHT_COLUMN_LEFT, pieceFactory.createPiece("Knight", PlayerSides.Sides.White)); // first player's Knights
		placePiece(WHITE_FIRST_ROW, KNIGHT_COLUMN_RIGHT, pieceFactory.createPiece("Knight", PlayerSides.Sides.White));
		placePiece(BLACK_FIRST_ROW, KNIGHT_COLUMN_LEFT, pieceFactory.createPiece("Knight", PlayerSides.Sides.Black)); // second player's Knights
		placePiece(BLACK_FIRST_ROW, KNIGHT_COLUMN_RIGHT, pieceFactory.createPiece("Knight", PlayerSides.Sides.Black));
		// add Knights to maps
		firstPlayerPieces.put(new Point(WHITE_FIRST_ROW,KNIGHT_COLUMN_LEFT), getPiece(WHITE_FIRST_ROW,KNIGHT_COLUMN_LEFT)); // add to first player's map
		firstPlayerPieces.put(new Point(WHITE_FIRST_ROW,KNIGHT_COLUMN_RIGHT), getPiece(WHITE_FIRST_ROW,KNIGHT_COLUMN_RIGHT));
		secondPlayerPieces.put(new Point(BLACK_FIRST_ROW,KNIGHT_COLUMN_LEFT), getPiece(BLACK_FIRST_ROW,KNIGHT_COLUMN_LEFT)); // add to second player's map
		secondPlayerPieces.put(new Point(BLACK_FIRST_ROW,KNIGHT_COLUMN_RIGHT), getPiece(BLACK_FIRST_ROW,KNIGHT_COLUMN_RIGHT));
		
		// place Bishops
		placePiece(WHITE_FIRST_ROW, BISHOP_COLUMN_LEFT, pieceFactory.createPiece("Bishop", PlayerSides.Sides.White)); // first player's Bishops
		placePiece(WHITE_FIRST_ROW, BISHOP_COLUMN_RIGHT, pieceFactory.createPiece("Bishop",PlayerSides.Sides.White));
		placePiece(BLACK_FIRST_ROW, BISHOP_COLUMN_LEFT, pieceFactory.createPiece("Bishop", PlayerSides.Sides.Black)); // second player's Bishops
		placePiece(BLACK_FIRST_ROW, BISHOP_COLUMN_RIGHT, pieceFactory.createPiece("Bishop", PlayerSides.Sides.Black));
		// add Bishops to maps
		firstPlayerPieces.put(new Point(WHITE_FIRST_ROW,BISHOP_COLUMN_LEFT), getPiece(WHITE_FIRST_ROW,BISHOP_COLUMN_LEFT)); // add to first player's map
		firstPlayerPieces.put(new Point(WHITE_FIRST_ROW,BISHOP_COLUMN_RIGHT), getPiece(WHITE_FIRST_ROW, BISHOP_COLUMN_RIGHT));
		secondPlayerPieces.put(new Point(BLACK_FIRST_ROW,BISHOP_COLUMN_LEFT), getPiece(BLACK_FIRST_ROW,BISHOP_COLUMN_LEFT)); // add to second player's map
		secondPlayerPieces.put(new Point(BLACK_FIRST_ROW,BISHOP_COLUMN_RIGHT), getPiece(BLACK_FIRST_ROW, BISHOP_COLUMN_RIGHT));
		
		// place Queens
		placePiece(WHITE_FIRST_ROW, QUEEN_COLUMN, pieceFactory.createPiece("Queen", PlayerSides.Sides.White)); // first player's Queen
		placePiece(BLACK_FIRST_ROW, QUEEN_COLUMN, pieceFactory.createPiece("Queen", PlayerSides.Sides.Black)); // second player's Queen
		// add Queens to maps
		firstPlayerPieces.put(new Point(WHITE_FIRST_ROW,QUEEN_COLUMN), getPiece(WHITE_FIRST_ROW,QUEEN_COLUMN)); // add to first player's map
		secondPlayerPieces.put(new Point(BLACK_FIRST_ROW,QUEEN_COLUMN), getPiece(BLACK_FIRST_ROW,QUEEN_COLUMN)); // add to second player's map
		
		// place Kings
		placePiece(WHITE_FIRST_ROW, KING_COLUMN, pieceFactory.createPiece("King", PlayerSides.Sides.White)); // first player's King
		placePiece(BLACK_FIRST_ROW, KING_COLUMN, pieceFactory.createPiece("King", PlayerSides.Sides.Black)); // second player's King
		// add Kings to maps
		firstPlayerPieces.put(new Point(WHITE_FIRST_ROW,KING_COLUMN), getPiece(WHITE_FIRST_ROW,KING_COLUMN)); // add to first player's map
		secondPlayerPieces.put(new Point(BLACK_FIRST_ROW,KING_COLUMN), getPiece(BLACK_FIRST_ROW,KING_COLUMN)); // add to second player's map
	}
	
	/**
	 * Gets a position and checks if at least one of the row or column indices exceed board's size
	 * @param row- row index to check
	 * @param col- column index to check
	 * @return True if position does not fit on board, False if fits
	 */
	@Override
	public boolean notValidPosition(int row, int col) {
		
		// check if given row and column indices do not exceed board boundaries
		if (row <= 0 || BOARD_SIZE < row || col <= 0 || BOARD_SIZE < col) {
			// position exceeds
			return true;
		}
		
		// position fits
		return false;
	}
	
	/**
	 * Gets a position and checks if does not exceed board's size
	 * @param row- row index to check
	 * @param col- column index to check
	 * @return True if position does not fit on board, False if fits
	 */
	@Override
	public boolean isValidPosition(int row, int col) {
		
		// check if given row and column indices do not exceed board boundaries
		if (row <= 0 || BOARD_SIZE < row || col <= 0 || BOARD_SIZE < col) {
			// position exceeds
			return false;
		}
		
		// position fits
		return true;
	}
	
	/**
	 * Scans piece map of first player and checks if current piece is the King.
	 * Once the King is found, its position is returned.
	 * If first player does not hold a King, null is returned
	 * @return position of first player's King, or null if first player does not have a King
	 */
	@Override
	public Point getFirstKingPosition() {

		// initialize positions for Kings
		Point kingPosition = null;
		
		// search first player's piece map and find King's position
		for (Point currPiecePosition : firstPlayerPieces.keySet()) {
			
			// check if current piece is the King
			if (firstPlayerPieces.get(currPiecePosition) instanceof King) {
				
				// King found, update its position in return value
				kingPosition = new Point(currPiecePosition);
			}
		}
		
		// return position found
		return kingPosition;
	}
	
	/**
	 * Scans piece map of second player and checks if current piece is the King.
	 * Once the King is found, its position is returned.
	 * If second player does not hold a King, null is returned
	 * @return position of second player's King, or null if second player does not have a King
	 */
	@Override
	public Point getSecondKingPosition() {
		
		// initialize positions for Kings
		Point kingPosition = null;
		
		// search second player's piece map and find King's position
		for (Point currPiecePosition : secondPlayerPieces.keySet()) {
			
			// check if current piece is the King
			if (secondPlayerPieces.get(currPiecePosition) instanceof King) {
				
				// King found, update its position in return value
				kingPosition = new Point(currPiecePosition);
			}
		}
		
		// return position found
		return kingPosition;
	}
	
	/**
	 * Searches first player's King and returns it.
	 * If first player does not have a King, returns null
	 * @return reference to first player's King
	 */
	@Override
	public Piece getFirstKing() {
		
		// get first player's King position
		Point firstKingPosition = getFirstKingPosition();
		
		// check if King has been captured
		if (firstKingPosition == null) {
			return null;
		}
		
		// return the piece at found position
		return getPiece(firstKingPosition.x, firstKingPosition.y);
	}
	
	/**
	 * Searches second player's King and returns it.
	 * If second player does not have a King, returns null
	 * @return reference to second player's King
	 */
	@Override
	public Piece getSecondKing() {
		
		// get second player's King position
		Point secondKingPosition = getSecondKingPosition();

		// check if King has been captured
		if (secondKingPosition == null) {
			return null;
		}
		
		// return the piece at found position
		return getPiece(secondKingPosition.x, secondKingPosition.y);
	}
	
	/**
	 * Returns piece at given position. If position is not valid, returns null
	 * @param row- row index
	 * @param col- column index
	 * @return piece at given position or null if does not exist
	 */
	@Override
	public Piece getPiece(int row, int col) {
		
		// check if given position is valid
		if (notValidPosition(row, col)) {
			return null;
		}
		
		// return piece at given valid position
		return chessBoard[BOARD_SIZE-row][col-1];
	}
	
	/**
	 * Checks if indicated player has a piece at given position on board.
	 * Returns True if indicated player has a piece at given position, False if not
	 */
	@Override
	public boolean hasPiece(PlayerSides.Sides side, Point posToCheck) {
		
		// check which player is indicated
		if (side.equals(PlayerSides.Sides.White)) {
			// check with first player
			return firstPlayerPieces.containsKey(posToCheck);
		}
		
		// check with second player
		return secondPlayerPieces.containsKey(posToCheck);
	}
	
	/**
	 * Moves piece at given source position to destination position, according to the indicated player.
	 * If there is no piece assigned to indicated player at source position, or destination is not a valid position,
	 * returns False. Otherwise, True
	 */
	@Override
	public boolean movePiece(PlayerSides.Sides currentPlayerSide, Point srcPosition, Point destPosition) {
		
		// check if destination is valid
		if (notValidPosition(destPosition.x, destPosition.y)) {
			
			// destination position is not valid, cannot move
			return false;
		}
		
		// check if indicated player has a piece at source position
		if (!hasPiece(currentPlayerSide, srcPosition)) {
			
			// no piece at source position, cannot move
			return false;
		}
		
		// check if opponent player has a piece at destination position
		if (hasPiece(PlayerSides.getOpponentSide(currentPlayerSide), destPosition)) {
			
			// remove piece from opponent's map
			if (currentPlayerSide.equals(PlayerSides.Sides.Black)) {
				// first player's piece is being captured
				firstPlayerPieces.remove(destPosition);
			} else {
				// second player's piece is being captured
				secondPlayerPieces.remove(destPosition);
			}			
		}
		
		// get piece at given point
		Piece pieceToMove = getPiece(srcPosition.x, srcPosition.y);		
		// move piece to given position
		placePiece(destPosition.x, destPosition.y, pieceToMove);
		// remove piece from previous position
		placePiece(srcPosition.x, srcPosition.y, null); 
		// update plyaer's piece map
		updatePlayerMap(currentPlayerSide, srcPosition, destPosition);
		return true;
	}

	/**
	 * Return a map of first player's players- key is position and value is piece
	 * @return reference to first player's piece map
	 */
	@Override
	public Map<Point, Piece> getFirstPlayerPieces() { return firstPlayerPieces; } 
	
	/**
	 * Return a map of second player's players- key is position and value is piece
	 * @return reference to second player's piece map
	 */
	@Override
	public Map<Point, Piece> getSecondPlayerPieces() { return secondPlayerPieces; }
	
	/**
	 * Adds to the board a piece of given type at given position. Updates given piece map with the new piece.
	 * If given type or position are invalid or there is another piece at given position, no piece is added and False is returned.
	 * Otherwise, adds the piece and returns True
	 */
	@Override
	public boolean addPiece(Point position, String type, PlayerSides.Sides side) {
		
		// try creating a new piece of given type
		Piece newPiece = pieceFactory.createPiece(type, side);
		
		// check if given type is invalid 
		if (newPiece == null) {
			// type is not valid
			return false;
		}

		// add piece
		return addPiece(position, newPiece);
	}
	
	/**
	 * Adds given piece at given position.
	 * If given position is not valid or there is another piece at given position,
	 * does not add the piece and False is returned. Otherwise, true is returned. 
	 * @param position- position of the new piece
	 * @param pieceToAdd- piece to be added
	 * @return True if could add piece, False if not
	 */
	@Override
	public boolean addPiece(Point position, Piece pieceToAdd) {
		
		// check if given position is valid
		if (notValidPosition(position.x, position.y)) {
			// position is not valid
			return false;
		}
	
		// check if given position is free
		if (getPiece(position.x, position.y) != null) {
			// position is not free
			return false;
		}
		
		// get relevant player's pieces
		Map<Point, Piece> playerPieces;
		if (pieceToAdd.getPlayer().equals(PlayerSides.Sides.White)) {
			playerPieces = firstPlayerPieces;
		} else {
			playerPieces = secondPlayerPieces;
		}
		
		// can create piece, update piece map
		playerPieces.put(position, pieceToAdd);
		
		// place piece on board
		placePiece(position.x, position.y, pieceToAdd);		
		return true;
	}
	
	/**
	 * Removes piece from given position.
	 * Returns True if could remove the piece, False if not
	 */
	@Override
	public boolean removePiece(Point position, PlayerSides.Sides side) {

		// get relevant player's pieces
		Map<Point, Piece> playerPieces;
		if (side.equals(PlayerSides.Sides.White)) {
			playerPieces = firstPlayerPieces;
		} else {
			playerPieces = secondPlayerPieces;
		}
		
		// check if indicated player has a piece at given position
		if (!playerPieces.containsKey(position)) {
			// no piece, cannot remove
			return false;
		}
		
		// remove piece from player's map
		playerPieces.remove(position);
		
		// update game board
		placePiece(position.x, position.y, null);
		
		// removed successfully
		return true;
	}
	
	/**
	 * Checks if given piece is at board's end. Board's end is determined according to
	 * the player the piece is assigned to
	 * @param piece- piece to check
	 * @param position- position of the piece
	 * @return True if piece is at board's end, False if not
	 */
	@Override
	public boolean isPieceAtEnd(Piece piece, Point position) {
		
		// determine board's end according to piece's player
		if (piece.getPlayer().equals(PlayerSides.Sides.White)) {
			
			// check if reached first row
			if (position.x == 1) {
				return true;
			}
			
		} else if (position.x == BOARD_SIZE) { // piece is assigned to second player
			// piece has reached board end
			return true;
		}
		
		// piece is not at board end
		return false;
	}
	
	/**
	 * Scans board and adds a new Creature in probability of 2% at each position
	 * that does not hold any piece.
	 * If adding a Creature where there is already another Creature, both Creature are destroyed
	 */
	@Override
	public void placeCreatures() {

		// initialize collided Creature positions
		collidedCreatures.clear();
		
		// current position on board
		Point currPosition = new Point();
		 
		// scan entire board
		for (int row = 1 ; row <= BOARD_SIZE ; ++row) {
			for (int col = 1 ; col <= BOARD_SIZE ; ++col) {
				 
			// check if current position is free
			if (getPiece(row, col) != null) {
				// move to next position
				continue;
			}
	
			// add a Creature in probability of 2%  
			if (RandomGenerator.nextInt(100) < CHANCE_FOR_CREATURE) {
					 
				// check if there is already another Creature at current position
				currPosition.move(row, col);
				if (creaturePositions.contains(currPosition)) {
						 
					// destroy creature
					creaturePositions.remove(currPosition);
					collidedCreatures.add(currPosition);
						 
					// move to next position
						continue;
					}
					 
					// add a new Creature
					creaturePositions.add(new Point(currPosition));
				}
			}
		}
	}

	/**
	 * Removes all Creatures from board
	 */
	public void removeCreatures() {
		
		// remove all Creatures
		creaturePositions.clear();
		
		// clear collided Creatures
		collidedCreatures.clear();
	}
	
	/**
	 * Returns positions of Creature in game
	 * @return positions of Creatures
	 */
	public List<Point> getCreaturePositions() { return creaturePositions; }
	
	/**
	 * Returns positions where two Creatures collided
	 * @return positions of collided Creatures
	 */
	public List<Point> getCollidedCreaturePositions() { return collidedCreatures; }
}